/*
 * Copyright (c) United Binary LLC.  All rights reserved.
 * 
 * This code is licensed under the MIT License
 * 
 * SEE: http://harnessit.codeplex.com/license
 * 
 */
using System;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Xml;

using UnitedBinary.Core.IO;
using UnitedBinary.Core.Utility.Text;

namespace UnitedBinary.HarnessIt
{
	/// <summary>
	///		TestProject class represents a HarnessIt unit test project.
	/// </summary>
	internal class TestProject
	{
		#region Member Variables
		
		private string [] assemblies = new string [0];
		private string filename ="";
		private string savedHash = "NOT SET";
		private string executionEngine = "UnitedBinary.HarnessIt.TestEngine.Local.LocalExecutionEngine";
		private string engineSettings = "";
		private TestProjectConfig config = new TestProjectConfig();
		private bool configOk = false;
		
		#endregion

		#region Construction
		/// <summary>
		///		Default Constructor.
		/// </summary>
		internal TestProject()
		{
		}
		
		#endregion

		#region Project Assemblies' Properties
		/// <summary>
		///		AssemblyNames is a string array of the assembly file names in the project.
		/// </summary>
		internal string [] AssemblyNames
		{
			get
			{
				return assemblies;
			}
			set 
			{
				if (SameAssemblies(value,assemblies))
				{
					return;
				}

				assemblies = value;
			}
		}

		private bool SameAssemblies(string [] n1, string [] n2)
		{
			//
			// If they have a different number of assemblies, that's different.
			//
			if (n1.Length != n2.Length)
			{
				return false;
			}

			//
			// If they are BOTH empty arrays, they are equal.
			//
			if (n1.Length == 0)
			{
				return true;
			}

			//
			// Convert all our strings to lower case in local copies. 
			//
			string [] s1 = new string [n1.Length];
			string [] s2 = new string [n2.Length];

			for (int i=0; i < s1.Length; i++)
			{
				s1[i] = n1[i].ToLower();
				s2[i] = n2[i].ToLower();
			}

			bool found = false;
			foreach(string s in s1)
			{
				found = false;
				foreach(string t in s2)
				{
					if (s == t)
					{
						found = true;
						break;
					}
				}
				if (!found)
				{
					return false;
				}
			}
			return true;
		}


		/// <summary>
		///		AssemblyCount is the number of assemblies in this project.
		/// </summary>
		internal int AssemblyCount
		{
			get
			{
				return assemblies.Length;
			}
		}

		#endregion

		#region File IO code
		/// <summary>
		///		Save saves the project to the file specified by the string file.
		/// </summary>
		/// <param name="file">File to save to.</param>
		internal void Save(string file)
		{
			ConfigOk = false;

			// Use the RelativePaths class to turn our full assembly
			//  path names into relative paths names (relative to this file).
			RelativePaths rp = new RelativePaths();
			rp.BasePath = rp.DirectoryFromFileName(Path.GetFullPath(file));

			filename = file;

			//
			// Open our project file for writing with an XMLTextWriter
			// and make it pretty.
			//
			XmlTextWriter tw = new XmlTextWriter(file,null);
			tw.Formatting = Formatting.Indented;
			try
			{
				//
				// Initialize our DOM and get access to the root <project> node.
				//
				XmlDocument dom = new XmlDocument();
				dom.LoadXml("<?xml version=\"1.0\" encoding=\"utf-8\" ?><project></project>");
				XmlNodeList list = dom.GetElementsByTagName("project");
				XmlNode projElt = list[0];

				//
				// Create the top level nodes and add them to the root.
				//
				XmlElement fileElt = dom.CreateElement("file");
				XmlElement AElt = dom.CreateElement("assemblies");
				XmlElement exeElt = dom.CreateElement("test_execution");

				projElt.AppendChild(fileElt);
				projElt.AppendChild(AElt);
				projElt.AppendChild(exeElt);

				//
				// Set the file version property. 1.0 for now.
				//
				XmlElement versionElt = dom.CreateElement("version");
				versionElt.InnerText = "1.0";
				fileElt.AppendChild(versionElt);

				//
				// Write the assemblies into the <assemblies> node.
				//
				foreach(string s in AssemblyNames)
				{
					XmlElement aElt = dom.CreateElement("assembly");
					aElt.InnerText = rp.GetRelativePath(s);
					AElt.AppendChild(aElt);
				}

				//
				// Store the execution engine settings.
				//
				XmlElement engineElt = dom.CreateElement("engine");
				XmlElement settingsElt = dom.CreateElement("settings");
				settingsElt.InnerXml = EngineSettings;
				engineElt.InnerText = ExecutionEngine;
				exeElt.AppendChild(engineElt);
				exeElt.AppendChild(settingsElt);

				//
				// Save the DOM to our file.
				//
				dom.WriteContentTo(tw);
				
				savedHash = ComputeSavedHash();

				//
				// Try to save the (optional) config file. It's not a 
				// big deal if this fails.
				//
				try
				{
					SaveConfig();					
				}
				catch(Exception e)
				{
					Debug.WriteLine("Error saving config file: " + e.Message);
				}

			}
			finally
			{
				//
				// Close up our file handles (can't count on the GC).
				//
				tw.Flush();
				tw.Close();
			}
		}

		/// <summary>
		///		Load loads the project from the specified file.
		/// </summary>
		/// <param name="file">File to load.</param>
		internal void Load(string file)
		{
			ConfigOk = false;
			file = Path.GetFullPath(file);
			FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read,FileShare.Read);
			try
			{
				//
				// Create an XML document to read and parse the XML project file.
				//
				XmlDocument dom = new XmlDocument();
				dom.Load(fs);

				//
				// Get and validate the file version.
				//
				XmlNodeList fileList = dom.GetElementsByTagName("file");
				if (fileList.Count != 1)
				{
					System.IO.FileLoadException e = new System.IO.FileLoadException(
						"Invalid file format. Could not access the file settings in project file.",
						file);
					throw e;
				}

				//
				// Search through the <file> node to get the file version. 
				// 
				bool versionFound = false;
				string version = "0.0";
				foreach(XmlNode n in fileList[0].ChildNodes)
				{
					if (n.Name == "version")
					{
						versionFound = true;
						version = n.InnerText;
					}
				}

				if (!versionFound)
				{
					System.IO.FileLoadException e = new System.IO.FileLoadException(
						"Invalid file format. Could not access the file version in project file.",
						file);
					throw e;
				}

				//
				// Get access to the <assemblies> node.
				//
				XmlNodeList nodes = dom.GetElementsByTagName("assemblies");
				if (nodes == null || nodes.Count != 1)
				{
					System.IO.FileLoadException e = new System.IO.FileLoadException(
						"Invalid file format. Could not access the assemblies list in project file.",
						file);
					throw e;
				}
				
				XmlNode Anode = nodes[0];

				// Set the global filename of the project.
				filename = file;

				// If there are no assemblies, we're done here.
				int count = Anode.ChildNodes.Count;
				if (count <= 0)
				{
					return;
				}

				//
				// Use our RelativePaths class to convert the relative paths
				// contained in the project file to full paths. Although we
				// could probably load the relative paths fine, they are ugly
				// in the HarnessIt view and reports.
				//
				RelativePaths rp = new RelativePaths();
				rp.BasePath = rp.DirectoryFromFileName(Path.GetFullPath(file));

				//
				// Read in each assembly name and store it in our array.
				//
				string [] vals = new string [count];
				for (int i=0; i < count; i++)
				{
					// Read the *full path* file names.
					vals[i] = rp.RebuildFullPath(Anode.ChildNodes[i].InnerText);
					Debug.WriteLine("HarnessIt: Read assembly name into project: " + vals[i]);
					if (vals[i] == null || vals[i].Length == 0)
					{
						System.IO.FileLoadException e = new System.IO.FileLoadException(
							"Invalid file format. Could not read assembly name in project file.",
							file);
						throw e;
					}
				}

				// 
				// All is good, we have an array of assembly names.  Store them in our class.
				//
				AssemblyNames = vals;
				this.savedHash = ComputeSavedHash();

				//
				// Get access to the <test_execution> node which contains the
				// test execution engine setting for this project.
				//
				XmlNodeList exeList = dom.GetElementsByTagName("test_execution");
				if (exeList.Count != 1)
				{
					System.IO.FileLoadException e = new System.IO.FileLoadException(
						"Invalid file format. Could not access the test execution properties in project file.",
						file);
					throw e;
				}

				//
				// Search through the child nodes to find the execution engine.
				//
				bool engineFound = false;
				foreach(XmlNode n in exeList[0].ChildNodes)
				{
					if (n.Name == "engine")
					{
						engineFound = true;
						ExecutionEngine = n.InnerText;
					}
					else if (n.Name == "settings")
					{	// Added in 1.01, don't require this unless we want to break old files.
						EngineSettings = n.InnerXml;
					}
				}

				if (!engineFound)
				{
					System.IO.FileLoadException e = new System.IO.FileLoadException(
						"Invalid file format. Could not access the execution engine in project file.",
						file);
					throw e;
				}

				//
				// Load the config file. This is a somewhat optional file
				// which stores details about tests were active and things
				// like that. Roughly equivalent to the .suo files in VS.NET.
				//
				try
				{
					LoadConfig();
				}
				catch(Exception e)
				{
					Debug.WriteLine("Error loading config file: " + e.Message);
				}
			}
			finally
			{
				// Close up our file handles (can't count on the GC).
				if (fs != null)
				{
					fs.Flush();
					fs.Close();
				}
			}
		}

		internal void SaveConfig()
		{
			if (FullPathName.Length >= 4)
			{
				string cfgFile = FullPathName.Remove(FullPathName.Length-4,4)+".HarnessItConfig";
				cfgFile = Path.GetFullPath(cfgFile);
				Config.Save(cfgFile);
				ConfigOk = true;
			}
		}

		internal void LoadConfig()
		{
			if (FullPathName.Length >= 4)
			{
				string cfgFile = FullPathName.Remove(FullPathName.Length-4,4)+".HarnessItConfig";
				cfgFile = Path.GetFullPath(cfgFile);
				Config.Load(cfgFile);
				ConfigOk = true;
			}
		}
		#endregion

		#region Project Properties
		internal string ExecutionEngine
		{
			get
			{
				return executionEngine;
			}
			set
			{
				executionEngine = value;
			}
		}

		internal string EngineSettings
		{
			get
			{
				return engineSettings;
			}
			set
			{
				engineSettings = value;
			}
		}

		/// <summary>
		///		Name is the short file name of the project (no directories).
		/// </summary>
		internal string Name
		{
			get
			{
				if (filename == "")
				{
					return "New Project";
				}

				int start = 0;
				int stop = filename.Length;
				for(int i=0; i < filename.Length - 1; i++)
				{
					if (filename[i] == '\\')
						start = i+1;
				}

				for(int i=filename.Length - 1; i > 1; i--)
				{
					if (filename[i] == '.')
					{
						stop = i - 1;
						break;
					}
				}

				string retval = "";
				for (int j=start; j <= stop; j++)
				{
					retval += filename[j].ToString();
				}
				return retval;
			}
		}

		internal TestProjectConfig Config
		{
			get
			{
				return config;
			}
		}

		internal string FullPathName
		{
			get
			{
				return filename;
			}
		}

		internal bool ConfigOk
		{
			get
			{
				return configOk;
			}
			set
			{
				configOk = value;
			}
		}

		internal bool Changed
		{
			get
			{
				return ComputeSavedHash() != this.savedHash;
			}
		}

		#endregion
	
		#region Change Monitoring Code

		public override string ToString()
		{
			System.Text.StringBuilder sb = new System.Text.StringBuilder();

			sb.Append("HarnessIt Project " + this.Name + "\n\n");
			sb.Append("Assemblies:\n");
			foreach (string a in assemblies)
			{
				sb.Append(a);
				sb.Append("\n");
			}
			sb.Append("\n");

			sb.Append("File Name:\n");
			sb.Append(filename);
			sb.Append("\n\n");

			sb.Append("Execution Engine:\n");
			sb.Append(executionEngine);
			sb.Append("\n\n");

			sb.Append("Execution Engine Settings:\n");
			sb.Append(engineSettings);
			sb.Append("\n");

			string toString = sb.ToString();

			//Debug.WriteLine("ToString returning:");
			//Debug.WriteLine(toString);

			return toString;
		}

		private string ComputeSavedHash()
		{
			MD5CryptoServiceProvider md5Hasher = new MD5CryptoServiceProvider();
			
			byte [] myStringBytes = System.Text.Encoding.ASCII.GetBytes(this.ToString());
			byte [] currentHash = md5Hasher.ComputeHash(myStringBytes);
			md5Hasher.Clear();

			string hashString = Format.BytesToHexString(currentHash);
			//Debug.WriteLine("ComputeSavedHash returning: " + hashString);

			return hashString;
		}

		#endregion
	
	}
}

